﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using Chat.Server;

namespace Chat.Client
{
    /// <summary>
    /// For each server command, received by the ClientEndPoint instanse
    /// (see ClientEndPoint class below) there is a personal event, risen upon
    /// this specific command received.
    /// This class is the base class for _all_ event arguments classes, each
    /// for a specific command received from the server.
    /// </summary>
    /// <typeparam name="Cmd">class inherited from Server.ServerCmd</typeparam>
    public abstract class ServerCmdEventArgs<Cmd> : CmdEventArgs<Cmd>
    where Cmd : Server.ServerCmd
    {
        public ServerCmdEventArgs(Cmd commandReceived) :
            base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Args for event, risen upon Server.MessageCmd command received.
    /// </summary>
    public class MessageEventArgs : ServerCmdEventArgs<Server.MessageCmd>
    {
        public MessageEventArgs(Server.MessageCmd commandReceived)
            : base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Args for event, risen upon Server.UsersOnlineCmd command received.
    /// </summary>
    public class UsersOnlineEventArgs : ServerCmdEventArgs<Server.UsersOnlineCmd>
    {
        public UsersOnlineEventArgs(Server.UsersOnlineCmd commandReceived)
            : base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Args for event, risen upon Server.UsersUpdateCmd command received.
    /// </summary>
    public class UsersUpdateEventArgs : ServerCmdEventArgs<Server.UsersUpdateCmd>
    {
        public UsersUpdateEventArgs(Server.UsersUpdateCmd usersUpdateCmd)
            : base(usersUpdateCmd)
        {
        }
    }
    /// <summary>
    /// Exception, thrown by ClientEndPoint when failed to login to the chat server.
    /// </summary>
    public class LoginFailedException : ApplicationException
    {
        public LoginFailedCmd LoginFailedCmd { get; protected set; }

        public LoginFailedException(LoginFailedCmd loginFailedCmd)
        {
            LoginFailedCmd = loginFailedCmd;
        }

        public override string Message
        {
            get
            {
                return LoginFailedCmd.Reason;
            }
        }
    }

    /// <summary>
    /// Implementation of the BaseEndPoint class on the client side.
    /// MUST SEE the BaseEndPoint class documentation before.
    /// </summary>
    public class ClientEndPoint : BaseEndPoint<ClientCmd>
    {
        /// <summary>
        /// Initializes the new instanse of the ClientEndPoint class with the
        /// specified login data, communication channel with the server and
        /// with the specified serializer that will be used by the client to
        /// communicate with the chat server over the communication channel.
        /// The ctor is responsible for sending the Login command to the server,
        /// using the specified connection and the specified serializer.
        /// </summary>
        /// <param name="loginData">The login data of the chat client</param>
        /// <param name="connection">
        /// The communication channel with the chat server.
        /// </param>
        /// <param name="serializer">
        /// The serializer that will be used when communicating with the server
        /// over the communication channel.
        /// </param>
        public ClientEndPoint(
            LoginData loginData,
            Connection<ClientCmd> connection,
            CmdSerializer<ClientCmd> serializer) :
            base(connection, serializer)
        {
            LoginData = loginData;

            // send login command to server
            // it would be nice to put a timeout here.
            SendCommand(new LoginCmd(LoginData), SendCommandMode.SendOnly);

            // expect to receive LoginResponseCmd in response to the login command.
            // It would be nice to put a timeout here too.
            object responseCmd = ReceiveCommand();

            // exception is the only legal way to return error codes from constructors
            if (responseCmd is LoginFailedCmd)
                throw new LoginFailedException(responseCmd as LoginFailedCmd);

            // we are expecting for either LoginFailedCmd or LoginOkCmd only
            if (!(responseCmd is LoginOkCmd))
                throw new UnknownCmdException(responseCmd);
        }

        /// <summary>
        /// Static wrapper for the ClientEndPoint ctor.
        /// Will close the specified connection if exception thrown from the ctor.
        /// </summary>
        /// <returns>The new ClientEndPoint instanse.</returns>
        public static ClientEndPoint Create(
            LoginData loginData,
            Connection<ClientCmd> connection,
            CmdSerializer<ClientCmd> serializer)
        {
            try
            {
                ClientEndPoint cep = new ClientEndPoint(
                    loginData, connection, serializer);

                return cep;
            }
            catch (Exception ex)
            {
                connection.Close();
                throw ex;
            }
        }

        /// <summary>
        /// Occurs, when chat text message received from the server.
        /// </summary>
        public event EventHandler<MessageEventArgs> MessageReceived;
        /// <summary>
        /// Occurs when Server.UsersOnline command received from the server.
        /// </summary>
        public event EventHandler<UsersOnlineEventArgs> UsersOnline;
        /// <summary>
        /// Occurs, when Server.UsersUpdate command received from the server.
        /// </summary>
        public event EventHandler<UsersUpdateEventArgs> UsersUpdate;

        /// <summary>
        /// Sends chat text message in SendCommandMode.DisposeThrow mode.
        /// </summary>
        public void SendMessage(NickName to, string message)
        {
            SendMessage(to, message, SendCommandMode.DisposeThrow);
        }

        /// <summary>
        /// Sends chat text message in specified SendCommandMode mode.
        /// </summary>
        public void SendMessage(NickName to, string message, SendCommandMode sendMode)
        {
            MessageData msgData = new MessageData(to, message, LoginData.User);
            MessageCmd cmd = new MessageCmd(msgData);

            SendCommand(cmd, sendMode);
        }

        /// <summary>
        /// Sends broadcast chat text message in SendCommandMode.DisposeThrow mode.
        /// </summary>
        public void SendMessage(string message)
        {
            SendMessage(message, SendCommandMode.DisposeThrow);
        }

        /// <summary>
        /// Sends broadcast chat text message in the specified SendCommandMode mode.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="sendMode"></param>
        public void SendMessage(string message, SendCommandMode sendMode)
        {
            SendMessage(NickName.Broadcast, message, sendMode);
        }

        /// <summary>
        /// Sends logout command to the server in SendCommandMode.DisposeThrow mode.
        /// Disposes self after the logout command successfully sent.
        /// </summary>
        public virtual void Logout()
        {
            try
            {
                SendCommand(new LogoutCmd(LoginData.User));
                Dispose();
            }
            catch (Exception)
            {
                //
                // no need to dispose sence we sent in SendCommandMode.DisposeThrow mode.
            }
        }

        /// <summary>
        /// Processes the chat command received from the server.
        /// For each valid command type raises event,
        /// specific for this type of server command.
        /// </summary>
        /// <param name="sender">The command receiver thread.</param>
        /// <param name="e">Event arguments containing the server command.</param>
        /// <returns>true, if the specified command is valid, false otherwise.</returns>
        protected override bool ProcessReceivedCommand(object sender, CmdReceivedEventArgs e)
        {
            if (e.CommandReceived is Server.UsersOnlineCmd)
                OnUsersOnline(new UsersOnlineEventArgs(
                    e.CommandReceived as Server.UsersOnlineCmd));
            else if (e.CommandReceived is Server.MessageCmd)
                OnMessageReceived(new MessageEventArgs(
                    e.CommandReceived as Server.MessageCmd));
            else if (e.CommandReceived is Server.UsersUpdateCmd)
                OnUsersUpdate(new UsersUpdateEventArgs(
                    e.CommandReceived as Server.UsersUpdateCmd));
            else
                return false;

            return true;
        }

        /// <summary>
        /// Rises UsersOnline event.
        /// </summary>
        /// <param name="e">The arguments of the event.</param>
        protected virtual void OnUsersOnline(UsersOnlineEventArgs e)
        {
            if (UsersOnline != null)
                UsersOnline(this, e);
        }
        /// <summary>
        /// Rises UsersUpdate event.
        /// </summary>
        /// <param name="e">The arguments of the event.</param>
        protected virtual void OnUsersUpdate(UsersUpdateEventArgs e)
        {
            if (UsersUpdate != null)
                UsersUpdate(this, e);
        }
        /// <summary>
        /// Rises MessageReceived event.
        /// </summary>
        /// <param name="e">The arguments of the event.</param>
        protected virtual void OnMessageReceived(MessageEventArgs e)
        {
            if (MessageReceived != null)
                MessageReceived(this, e);
        }
    }
}
